﻿using System;
using System.Collections.Generic;
using System.Text;

using System.Net.Sockets;
using System.IO;
using System.ComponentModel;
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;

using Mono.Security.Protocol.Ntlm;

namespace SMTPDialog
{
    public class SMTPMailer
    {
        public class SmtpException : System.Exception
        {
            public SmtpException(string message)
                : base(message)
            { }

            public SmtpException(string message, Exception ex)
                : base(message, ex)
            { }
        }

        const int TIMEOUT = 10;
        
        string smtpServerAddr;
        int smtpServerPort;
        bool useTls = false;
        bool extendedSmtp = false;
        bool auth;
        string authMethod;
        bool authCurrent;
        string authDomain, authUser, authPass;
        string smtpDomain;
        string mailFrom;
        string rcptTo;
        string data;
        string sessionLog;

        string serverCertStatus;

        BackgroundWorker worker;
        TcpClient smtpTcp;
        NetworkStream streamSmtp;
        SslStream sslStream;
        StreamReader smtpReader;
        StreamWriter smtpWriter;

        public string SmtpServerAddr 
        {
            get { return smtpServerAddr; }
            set { smtpServerAddr = value;  }
        }

        public int SmtpServerPort
        {
            get { return smtpServerPort; }
            set { smtpServerPort = value; }
        }

        public bool UseTLS
        {
            get { return useTls; }
            set { useTls = value; }
        }

        public bool ExtendedSmtp
        {
            get { return extendedSmtp; }
            set { extendedSmtp = value; }
        }

        public bool Auth
        {
            get { return auth; }
            set { auth = value; }
        }

        public string AuthMethod
        {
            get { return authMethod; }
            set { authMethod = value; }
        }

        public bool AuthCurrent
        {
            get { return authCurrent; }
            set { authCurrent = value; }
        }

        public string AuthDomain
        {
            get { return authDomain; }
            set { authDomain = value; }
        }

        public string AuthUser
        {
            get { return authUser; }
            set { authUser = value; }
        }

        public string AuthPass
        {
            get { return authPass; }
            set { authPass = value; }
        }

        public string SmtpDomain
        {
            get { return smtpDomain; }
            set { smtpDomain = value; }
        }

        public string MailFrom
        {
            get { return mailFrom; }
            set { mailFrom = value; }
        }

        public string RcptTo
        {
            get { return rcptTo; }
            set { rcptTo = value; }
        }

        public string Data
        {
            get { return data; }
            set { data = value; }
        }

        public string SessionLog
        {
            get { return sessionLog; }
            set { sessionLog = value; }
        }

        public BackgroundWorker Worker
        {
            set { worker = value; }
        }

        public SMTPMailer()
        { }

        public SMTPMailer(string serverAddr, int serverPort, bool tls, bool extended)
        {
            smtpServerAddr = serverAddr;
            smtpServerPort = serverPort;
            useTls = tls;
            extendedSmtp = extended;
        }

        public string[] GetAuthMethods()
        {
            // Connect to server and initialize streams
            smtpTcp = new TcpClient();
            try
            {
                smtpTcp.Connect(smtpServerAddr, smtpServerPort);
            }
            catch (Exception ex)
            {
                ThrowException(ex.Message);
            }

            streamSmtp = smtpTcp.GetStream();
            smtpReader = new StreamReader(streamSmtp);
            smtpWriter = new StreamWriter(streamSmtp);

            // Wait for 220 and then send HELO/EHLO
            if (WaitResponse("220"))
                WriteLine("EHLO " + smtpDomain);
            else
                ThrowException("SMTP ERROR: 220 not received after connect.");

            // Note that AUTH methods offered by server
            // can be different after STARTTLS
            if (useTls)
            {
                // Wait for 250 and then send STARTTLS
                if (WaitResponse("250"))
                    WriteLine("STARTTLS");
                else
                    ThrowException("SMTP ERROR: 250 not received after EHLO.");

                // Wait for 220 after STARTTLS
                if (!WaitResponse("220"))
                    ThrowException("SMTP ERROR: 220 not received after STARTTLS.");

                sslStream = new SslStream(
                    smtpTcp.GetStream(),
                    false,
                    new RemoteCertificateValidationCallback(ValidateServerCertificate),
                    null);
                serverCertStatus = ""; // This string will hold SSL errors detected by the callback
                try
                {
                    sslStream.AuthenticateAsClient(smtpServerAddr);
                }
                catch (System.Exception ex)
                {
                    ThrowException("SSL ERROR: can't authenticate server: " + ex.Message);
                }

                // Send certificate to SmtpForm and reinstance read and write streams
                // over encrypted connection
                sessionLog += serverCertStatus;
                worker.ReportProgress(0, serverCertStatus);
                worker.ReportProgress(10, sslStream.RemoteCertificate);
                smtpReader = new StreamReader(sslStream);
                smtpWriter = new StreamWriter(sslStream);

                // Finally send EHLO over encrypted connection
                WriteLine("EHLO " + smtpDomain);
            }

            // Read first line, which should say 250
            string line = ReadLine();
            worker.ReportProgress(0, line);
            if (line.Substring(0, 3) != "250")
                return null;

            // Gather additional lines with ESMTP options and find AUTH
            string response = "";
            string[] authMethods = new string[] { };
            while (line[3] == '-')
            {
                line = smtpReader.ReadLine();
                if (line.Length < 8)
                    continue;
                if (line.Substring(4, 4) == "AUTH")
                {
                    authMethods = line.Substring(8).Trim().Split(new char[] { ' ' });
                }
                response += line + Environment.NewLine;
            }
            sessionLog += response;
            worker.ReportProgress(0, response);

            // Close connection
            WriteLine("QUIT");
            smtpTcp.Close();
            return authMethods;
        }

        public void SendMail()
        {

            // Connect to server and initialize streams
            smtpTcp = new TcpClient();
            try
            {
                smtpTcp.Connect(smtpServerAddr, smtpServerPort);
            }
            catch (Exception ex)
            {
                ThrowException(ex.Message);
            }

            streamSmtp = smtpTcp.GetStream();
            smtpReader = new StreamReader(streamSmtp);
            smtpWriter = new StreamWriter(streamSmtp);

            // Wait for 220
            if (!WaitResponse("220"))
                ThrowException("SMTP ERROR: 220 not received after connect.");

            // Send HELO/EHLO and wait for 250
            if (CancellationPending())
                return;
            WriteLine((extendedSmtp ? "EHLO " : "HELO ") + smtpDomain);
            if (!WaitResponse("250"))
                ThrowException("ERROR SMTP: No se ha recibido 250 tras HELO/EHLO.");

            if (useTls)
            {
                // Send STARTTLS and wait for 220
                if (CancellationPending())
                    return;
                WriteLine("STARTTLS");
                if (!WaitResponse("220"))
                    ThrowException("ERROR SMTP: No se ha recibido 220 tras STARTTLS.");

                // Start SSL stream
                if (CancellationPending())
                    return;
                sslStream = new SslStream(
                    smtpTcp.GetStream(),
                    false,
                    new RemoteCertificateValidationCallback(ValidateServerCertificate),
                    null);
                serverCertStatus = ""; // This string will hold SSL errors detected by the callback
                try
                {
                    sslStream.AuthenticateAsClient(smtpServerAddr);
                }
                catch (System.Exception ex)
                {
                    ThrowException("ERROR SSL: no se puede autenticar servidor: " + ex.Message);
                }

                // Send certificate to SmtpForm and reinstance read and write streams
                // over encrypted connection
                sessionLog += serverCertStatus;
                worker.ReportProgress(0, serverCertStatus);
                worker.ReportProgress(10, sslStream.RemoteCertificate);
                smtpReader = new StreamReader(sslStream);
                smtpWriter = new StreamWriter(sslStream);

                // Finally send EHLO over encrypted connection
                if (CancellationPending())
                    return;
                WriteLine("EHLO " + smtpDomain);
                if (!WaitResponse("250"))
                    ThrowException("SMTP ERROR: 250 not received after HELO/EHLO.");
            }

            // Authenticate
            if (auth)
            {
                if (authMethod == "LOGIN")
                {
                    if (CancellationPending())
                        return;  
                    WriteLine("AUTH LOGIN");
                    // VXNlcm5hbWU6 means Username: in base64
                    if (!WaitResponse("334 VXNlcm5hbWU6"))
                        ThrowException("ERROR SMTP: 334 Username: not received after AUTH LOGIN.");
                    
                    if (CancellationPending())
                        return;
                    string authUserDom = authDomain.Contains(".") ?
                        authUser + '@' + authDomain : // Domain FQDN
                        authDomain + '\\' + authUser; // Domain NetBIOS name
                    WriteLine(Convert.ToBase64String(
                        System.Text.ASCIIEncoding.ASCII.GetBytes(authUserDom)));
                    // UGFzc3dvcmQ6 means Password: in base64
                    if (!WaitResponse("334 UGFzc3dvcmQ6"))
                        ThrowException("ERROR SMTP: 334 Password: not received after username.");

                    if (CancellationPending())
                        return;
                    WriteLine(Convert.ToBase64String(
                        System.Text.ASCIIEncoding.ASCII.GetBytes(authPass)));
                }

                else if (authMethod == "NTLM")
                {
                    // Reference for NTLM authentication
                    // http://davenport.sourceforge.net/ntlm.html
                    // Using various project Mono source files, see Mono_readme.txt

                    // Send type 1 message
                    Type1Message type1 = new Type1Message();
                    type1.Domain = authDomain;
                    if (CancellationPending())
                        return;
                    WriteLine("AUTH NTLM " + Convert.ToBase64String(type1.GetBytes()));

                    // Send type 2 message after 334
                    string line = ReadLine();
                    worker.ReportProgress(0, line + Environment.NewLine);
                    if (line.Substring(0, 3) != "334")
                        ThrowException("SMTP ERROR: 334 not received after AUTH NTLM type1.");
                    Type2Message type2 = new Type2Message(Convert.FromBase64String(line.Substring(4)));

                    // Send type 3 message
                    Type3Message type3 = new Type3Message();
                    type3.Domain = authDomain;
                    type3.Username = authUser;
                    type3.Challenge = type2.Nonce;
                    type3.Password = authPass;
                    WriteLine(Convert.ToBase64String(type3.GetBytes()));
                }

                // If authentication succeeded, I should read 235
                if (!WaitResponse("235"))
                    ThrowException("SMTP ERROR: 235 not received after AUTH");
            }

            // Send From envelop address (P1) and wait for 250
            if (CancellationPending())
                return;
            WriteLine("MAIL FROM: " + mailFrom);
            if (!WaitResponse("250"))
                ThrowException("SMTP ERROR: 250 not received after MAIL FROM.");

            // Send To envelop address (P1) and wait for 250
            if (CancellationPending())
                return;
            WriteLine("RCPT TO: " + rcptTo);
            if (!WaitResponse("250"))
                ThrowException("SMTP ERROR: 250 not received after RCPT TO.");

            // Send DATA command and wait for 354
            if (CancellationPending())
                return;
            WriteLine("DATA");
            if (!WaitResponse("354"))
                ThrowException("SMTP ERROR: 354 not received after DATA.");

            // Send message and wait for 250
            if (CancellationPending())
                return;
            WriteLine(data);
            if (!WaitResponse("250"))
                ThrowException("SMTP ERROR: 250 not received after sending message");
                

            // Send QUIT and close connection
            WriteLine("QUIT");
            smtpTcp.Close();
        }

        private void WriteLine(string line)
        {
            sessionLog += line + Environment.NewLine;
            worker.ReportProgress(0, line + Environment.NewLine);
            smtpWriter.WriteLine(line);
            smtpWriter.Flush();
        }

        private void ThrowException(string message)
        {
            if (smtpTcp != null)
            {
                if (smtpWriter != null)
                    WriteLine("QUIT");  
                smtpTcp.Close();
            }
            throw new SmtpException(message);
        }

        private bool CancellationPending()
        {
            if (worker.CancellationPending)
            {
                WriteLine("QUIT");
                smtpTcp.Close();
                return true;
            }
            return false;
        }

        private bool WaitResponse(string code)
        {
            // Read first line and check for expected SMTP code
            bool success = false;
            string line = ReadLine();
            string response = line + Environment.NewLine;
            if (line.Substring(0, code.Length) == code)
                success = true;

            // Read additional lines with ESMTP options
            while (line[3] == '-')
            {
                line = smtpReader.ReadLine();
                response += line + Environment.NewLine;
            }
            sessionLog += response;
            worker.ReportProgress(0, response);
            return success;
        }

        // Read a line from stream with timeout
        private string ReadLine()
        {
            // Timeout in case expected answer is not received
            DateTime startTime = DateTime.Now;
            TimeSpan elapsedTime = DateTime.Now - startTime;

            bool received = false;
            string line = "";
            while (elapsedTime.Seconds < TIMEOUT && !received)
            {
                if (streamSmtp.DataAvailable)
                {
                    line = smtpReader.ReadLine();
                    received = true;
                }
                elapsedTime = DateTime.Now - startTime;
            }
            return line;
        }

        private bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain,
            SslPolicyErrors sslPolicyErrors)
        {
            // .NET 2 compatible flags enums comparison
            if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateChainErrors) == SslPolicyErrors.RemoteCertificateChainErrors)
                serverCertStatus += "SSL WARNING: Can't validate certification path, root certificate missing." + Environment.NewLine;
            if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNameMismatch) == SslPolicyErrors.RemoteCertificateNameMismatch)
               serverCertStatus += "SSL WARNING: Certificate name doesn't match host name." + Environment.NewLine;
            if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNotAvailable) == SslPolicyErrors.RemoteCertificateNotAvailable)
            {
                // If there is no certificate, there's little I can do here, so return false
                serverCertStatus += "SSL ERROR: Server certificate not available." + Environment.NewLine;
                return false;
            }

            // Accept certificate as valid, even with warnings
            return true;
        }

    }
}
